﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MetroGA.Core.Hits;

namespace MetroGA.Core
{
	internal abstract class AnalyticsCore : IMetroAnalytics
	{
		protected const string AnalyticsBaseUri = "https://ssl.google-analytics.com/collect";

		private static readonly object LockObject = new object();
		private static readonly ManualResetEventSlim InitializedEvent = new ManualResetEventSlim(false);

		private readonly Queue<Hit> _hits = new Queue<Hit>();
		protected readonly IPlatformInformationProvider InformationProvider;
		protected readonly BaseSessionContainer SessionContainer;
		protected readonly string TrackerId;
		protected readonly string ApplicationId;
		protected readonly string ApplicationName;
		protected readonly string ApplicationVersion;

		internal AnalyticsCore(IPlatformInformationProvider informationProvider, BaseSessionContainer sessionContainer, string trackerId, string applicationId, string applicationName, string applicationVersion)
		{
			InformationProvider = informationProvider;
			SessionContainer = sessionContainer;
			TrackerId = trackerId;
			ApplicationId = applicationId;
			ApplicationName = applicationName;
			ApplicationVersion = applicationVersion;

			Task.Factory.StartNew(DequeueHits, TaskCreationOptions.LongRunning);
			Task.Factory.StartNew(SaveHits, TaskCreationOptions.LongRunning);
		}

		protected async Task Initialize()
		{
			var hits = await LoadPersistedHitsAsync();
			foreach (var hit in hits)
			{
				_hits.Enqueue(hit);
			}

			InitializedEvent.Set();
		}

		private void SaveHits()
		{
			InitializedEvent.Wait();
			while (true)
			{
				lock (LockObject)
				{
					Monitor.Wait(LockObject);
					PersistHits(_hits);
				}
			}
		}

		protected abstract Task<IEnumerable<Hit>> LoadPersistedHitsAsync();
		protected abstract void PersistHits(IEnumerable<Hit> hits);

		//Loop to send hits to the server
		//Dequeue only if network is available
		//Re-enqueue failed
		private async void DequeueHits()
		{
			InitializedEvent.Wait();
			while (true)
			{
				var hits = new List<Hit>();
				lock (LockObject)
				{
					while (!_hits.Any() || !InformationProvider.IsNetworkAvailable)
						Monitor.Wait(LockObject, TimeSpan.FromMinutes(1));

					while (_hits.Any())
					{
						hits.Add(_hits.Dequeue());
					}
				}

				foreach (var hit in hits.ToList())
				{
					try
					{
						if (await SendHit(hit))
							hits.Remove(hit);
					}
					catch (Exception) { }
				}

				lock (LockObject)
				{
					foreach (var failedHit in hits)
						_hits.Enqueue(failedHit);

					Monitor.PulseAll(LockObject);
				}
			}
		}

		private void TrackHit(Hit hit)
		{
			hit.TrackingId = TrackerId;
			hit.ApplicationId = ApplicationId;
			hit.ApplicationName = ApplicationName;
			hit.ApplicationVersion = ApplicationVersion;
			hit.CreationTime = DateTime.Now;

			var resolution = InformationProvider.Resolution;
			hit.ScreenResolution = String.Format("{0}x{1}", resolution.Item1, resolution.Item2);

			hit.UserLanguage = InformationProvider.Culture;

			hit.VisitorId = SessionContainer.VisitorId;
			var hitCount = SessionContainer.IncrementHitCount();
			hit.IsNewSession = hitCount == 0;
			hit.HitCount = hitCount;

			lock (LockObject)
			{
				_hits.Enqueue(hit);
				Monitor.PulseAll(LockObject);
			}
		}

		protected abstract Task<bool> SendHit(Hit hit);

		#region Implementation of IMetroAnalytics

		public void TrackView(string viewName)
		{
			TrackHit(new ViewHit { ViewName = viewName });
		}

		public void TrackView(Type viewType)
		{
			TrackHit(new ViewHit { ViewName = viewType.FullName });
		}

		public void TrackEvent(string category, string action, string label = null, long? value = null, string viewName = null, Type viewType = null)
		{
			TrackHit(new EventHit
				{
					Category = category,
					Action = action,
					Label = label,
					Value = value,
					ViewName = viewName ?? (viewType != null ? viewType.FullName : null)
				});
		}

		public void TrackException(string message, bool isFatal = false)
		{
			TrackHit(new ExceptionHit
				{
					Message = message,
					IsFatal = isFatal
				});
		}

		public void TrackException(Exception exception, bool isFatal = false)
		{
			TrackHit(new ExceptionHit
			{
				Message = exception.Message,
				IsFatal = isFatal
			});
		}

		public void TrackTiming(string category, string name, TimeSpan interval, string label = null)
		{
			TrackHit(new TimingHit
				{
					Category = category,
					Name = name,
					Interval = interval,
					Label = label
				});
		}

		public void TrackSocial(string network, string action, string target = null)
		{
			TrackHit(new SocialHit
				{
					Network = network,
					Action = action,
					Target = target
				});
		}

		public void RenewSession()
		{
			SessionContainer.Destroy();
		}

		#endregion

	}
}
